
using System.Collections;
using KeraLua;

namespace Ex
{
    public class EnvLua
    {
        public class GlobalField : IExTable
        {
            private readonly EnvLua env;
            public GlobalField(EnvLua lua)
            {
                this.env = lua;
            }


            public void Set(object key, object value)
            {
                lock (env){
                    env.PushObjectToLuaStack(value);
                    env.Current.SetGlobal((string)key);
                }
            }

            // public void SetTable(object key)
            // {
            //     lua.NewTable();
            //     lua.SetGlobal((string)key);
            // }

            public object Get(object key)
            {
                lock (env)
                {
                    env.Current.GetGlobal((string)key);
                    return env.FetchObjectFromLuaStack();
                }
            }

            public IDictionaryEnumerator GetEnumerator()
            {
                throw new NotImplementedException("can't get enumerator through GlobalField");
            }
        }

#if DEBUG
            public int StackLength { get => lua.GetTop(); }
#endif


        private Lua lua;

        public readonly GlobalField global;

        public Lua Current { get => lua; }

        public EnvLua(Lua lua) : base()
        {
            this.lua = lua;
            this.global = new(this);
            this.lua.AtPanic(WhenPanic);
        }

        public virtual EnvLua Copy()
        {
            return new(lua);
        }

        public virtual IExCsFunction WatchCsFunction(int idx)
        {
            return ExCsFunctionRef.WatchFromLuaStack(this, idx);
        }

        public virtual IExCsFunction FetchCsFunction() {
            return ExCsFunctionRef.FetchFromLuaStack(this);
        }

        public virtual IExLuaFunction WatchLuaFunction(int idx) {
            return ExLuaFunction.WatchFromLuaStack(this, idx);
        }

        public virtual IExLuaFunction FetchLuaFunction() {
            return ExLuaFunction.FetchFromLuaStack(this);
        }

        public virtual IExTable WatchTable(int idx) {
            return ExRefTable.WatchFromLuaStack(this, idx);
        }

        public virtual IExTable FetchTable() {
            return ExRefTable.FetchFromLuaStack(this);
        }


        public IEnumerable<string> GetStackInfo()
        {
            List<string> info = new();
            LuaDebug debug_struct = new();
            int level = 0;
            lock (this)
            {
                while (lua.GetStack(level, ref debug_struct) == 1)
                {
                    lua.GetInfo("nlS", ref debug_struct);
                    string source = debug_struct.ShortSource ?? "";
                    string func_line = debug_struct.CurrentLine == -1 ? "?" : debug_struct.CurrentLine.ToString();
                    string func_name, func_class;
                    if (debug_struct.What == "main")
                    {
                        func_name = "main";
                        func_class = "";
                    }
                    else
                    {
                        func_name = debug_struct.Name ?? "%Unknown%";
                        func_class = debug_struct.NameWhat ?? "";
                    }
                    info.Add($"{func_class} {func_name} ({func_line}): {source}\n");
                    level++;
                }
            }
            return info;
        }

        public virtual void Restart()
        {
            lua.Close();
            lua = new();
        }

        /// <summary>
        /// 使lua程序panic
        /// </summary>
        public void Error(string err_msg)
        {
            lua.Error(err_msg);
        }

        /// <summary>
        /// 无保护模式下调用lua触发panic会调用
        /// </summary>
        /// <param name="ptr"></param>
        /// <returns></returns>
        public int WhenPanic(IntPtr ptr)
        {
            string error_msg;
            try
            {
                // 安全地尝试获取错误消息
                error_msg = lua.ToString(-1);
            }
            catch (Exception e)
            {
                error_msg = e.Message;
            }
            finally
            {
                Restart();
            }
            throw new LuaException($"lua panic: {error_msg}", this);
        }

        public virtual void RunFile(string file_path)
        {
            lock (this)
            {
                if (Current.LoadFile(file_path) != LuaStatus.OK)
                {
                    throw new LuaException("LoadFile failed:" + Current.ToString(-1), this);
                }
                if (Current.PCall(0, 0, 0) != LuaStatus.OK)
                {
                    throw new LuaException("PCall failed:" + Current.ToString(-1), this);
                }
            }
        }

        public void RunFile(string file_path, IExTable env)
        {
            lock (this)
            {
                var htable = Tool.IExTableToStrDictionary(env);
                RunFile(file_path, htable);
            }
        }

        public virtual void RunFile(string file_path, IDictionary<string, object> env)
        {
            Hashtable raw_env = new();
            lock (this)
            {
                foreach (var key in env.Keys) {
                    raw_env[key] = global.Get(key);
                    global.Set(key, env[key] ?? UsableLuaType.nil);
                }
                RunFile(file_path);
                foreach (var key in env.Keys) {
                    global.Set(key, raw_env[key] ?? UsableLuaType.nil);
                }
            }
        }

        public IndefiniteVarType RunFunctionFile(string file_path, int return_num, params object[] param_args){
            return RunFunctionFileByArray(file_path, return_num, param_args);
        }

        public virtual IndefiniteVarType RunFunctionFileByArray(string file_path, int return_num, object[] param_args) {
            var param = IndefiniteVarType.FromArray(param_args);
            lock (this)
            {
                if (Current.LoadFile(file_path) != LuaStatus.OK)
                {
                    throw new LuaException("LoadFile failed:" + Current.ToString(-1), this);
                }
                param.PushToLuaStack(this);
                if (Current.PCall(param.Count, return_num, 0) != LuaStatus.OK)
                {
                    throw new LuaException("PCall failed:" + Current.ToString(-1), this);
                }
                return IndefiniteVarType.FetchFromLuaStack(this, return_num);
            }
        }

        public IndefiniteVarType RunFunctionFileWithEnv(string file_path, int return_num, IExTable env, params object[] param_args)
        {
            return RunFunctionFileWithEnvByArray(file_path, return_num, Tool.IExTableToStrDictionary(env), param_args);
        }

        public IndefiniteVarType RunFunctionFileWithEnv(string file_path, int return_num, IDictionary<string, object> env, params object[] param_args)
        {
            return RunFunctionFileWithEnvByArray(file_path, return_num, env, param_args);
        }

        public virtual IndefiniteVarType RunFunctionFileWithEnvByArray(string file_path, int return_num, IDictionary<string, object> env, object[] param_args)
        {
            Hashtable raw_env = new();
            lock (this)
            {
                foreach (var key in env.Keys) {
                    raw_env[key] = global.Get(key);
                    global.Set(key, env[key] ?? UsableLuaType.nil);
                }
                var result = RunFunctionFileByArray(file_path, return_num, param_args);
                foreach (var key in env.Keys) {
                    global.Set(key, raw_env[key] ?? UsableLuaType.nil);
                }
                return result;
            }
        }

        public virtual void DoString(string file_str)
        {
            lock (this)
            {
                if (Current.DoString(file_str))
                {
                    // throw new LuaException("Run lua code failed", this);
                    return;
                }
            }
        }

        public void DoString(string file_str, IExTable env)
        {
            lock (this)
            {
                var htable = Tool.IExTableToStrDictionary(env);
                DoString(file_str, htable);
            }
        }

        public virtual void DoString(string file_str, IDictionary<string, object> env)
        {
            lock (this)
            {
                Hashtable raw_env = new();
                foreach (var key in env.Keys) {
                    raw_env[key] = global.Get(key);
                    global.Set(key, env[key] ?? UsableLuaType.nil);
                }
                DoString(file_str);
                foreach (var key in env.Keys) {
                    global.Set(key, raw_env[key] ?? UsableLuaType.nil);
                }
            }
        }

        public IndefiniteVarType DoString(string file_str, int return_num, params object[] param_args){
            return DoStringByArray(file_str, return_num, param_args);
        }

        public virtual IndefiniteVarType DoStringByArray(string file_str, int return_num, object[] param_args) {
            var param = IndefiniteVarType.FromArray(param_args);
            lock (this)
            {
                if (Current.LoadString(file_str) != LuaStatus.OK)
                {
                    throw new LuaException("Load lua code failed:" + Current.ToString(-1), this);
                }
                param.PushToLuaStack(this);
                if (Current.PCall(param.Count, return_num, 0) != LuaStatus.OK)
                {
                    throw new LuaException("PCall failed:" + Current.ToString(-1), this);
                }
                return IndefiniteVarType.FetchFromLuaStack(this, return_num);
            }
        }

        public IndefiniteVarType DoStringWithEnv(string file_str, int return_num, IExTable env, params object[] param_args)
        {
            return DoStringWithEnvByArray(file_str, return_num, Tool.IExTableToStrDictionary(env), param_args);
        }

        public IndefiniteVarType DoStringWithEnv(string file_str, int return_num, IDictionary<string, object> env, params object[] param_args)
        {
            return DoStringWithEnvByArray(file_str, return_num, env, param_args);
        }

        public virtual IndefiniteVarType DoStringWithEnvByArray(string file_str, int return_num, IDictionary<string, object> env, object[] param_args)
        {
            Hashtable raw_env = new();
            lock (this)
            {
                foreach (var key in env.Keys) {
                    raw_env[key] = global.Get(key);
                    global.Set(key, env[key] ?? UsableLuaType.nil);
                }
                var result = DoStringByArray(file_str, return_num, param_args);
                foreach (var key in env.Keys) {
                    global.Set(key, raw_env[key] ?? UsableLuaType.nil);
                }
                return result;
            }
        }


        public void PushObjectToLuaStack(object obj, UsableLuaType type = UsableLuaType.any)
        {
            lock (this)
            {
                if (type == UsableLuaType.any)
                {
                    type = Tool.GetCsToLuaType(obj);
                }
                switch (type)
                {
                    case UsableLuaType.nil:
                        lua.PushNil();
                        break;
                    case UsableLuaType.boolean:
                        lua.PushBoolean((bool)obj);
                        break;
                    case UsableLuaType.number:
                        switch (obj)
                        {
                            case double d: lua.PushNumber(d); break;
                            case float f: lua.PushNumber(f); break;
                            case int i: lua.PushInteger(i); break;
                            case short i: lua.PushInteger(i); break;
                            default: lua.PushNumber((double)obj); break;
                        }
                        break;
                    case UsableLuaType.luaString:
                        lua.PushString((string)obj);
                        break;
                    case UsableLuaType.cfunction:
                    case UsableLuaType.function:
                    case UsableLuaType.table:
                    case UsableLuaType.csobject:
                        {
                            ((IExElement)obj).PushToLuaStack();
                        }
                        break;
                    default:
                        throw new InvalidDataException("unsupport object:" + obj);
                }
            }
        }


        public object WatchObjectFromLuaStack(UsableLuaType type, int index)
        {
            lock (this)
            {
                if (type == UsableLuaType.any) {
                    type = Tool.GetTypeOnStack(lua, index);
                }
                return type switch
                {
                    UsableLuaType.nil => UsableLuaType.nil,
                    UsableLuaType.boolean => lua.ToBoolean(index),
                    UsableLuaType.number => lua.ToNumber(index),
                    UsableLuaType.luaString => lua.ToString(index),
                    UsableLuaType.cfunction => WatchCsFunction(index),
                    UsableLuaType.function => WatchLuaFunction(index),
                    UsableLuaType.table => WatchTable(index),
                    UsableLuaType.csobject => ExCsRawData.WatchFromLuaStack(this, index),
                    _ => throw new InvalidOperationException(string.Format("Wrong type match, it expected to be {0}, but actually is {1}", type, Tool.GetTypeOnStack(lua, index)))
                };
            }
        }

        /// <summary>
        ///抓取堆栈最上层的值
        /// </summary>
        /// <exception cref="InvalidDataException"></exception>
        public object FetchObjectFromLuaStack(UsableLuaType type = UsableLuaType.any)
        {
            lock (this)
            {
                if (type == UsableLuaType.any)
                {
                    type = Tool.GetTypeOnStack(this.lua, -1);
                }
                object? result = null;
                switch (type)
                {
                    case UsableLuaType.nil:
                        result = UsableLuaType.nil;
                        lua.Pop(1);
                        break;
                    case UsableLuaType.boolean:
                        result = lua.ToBoolean(-1);
                        lua.Pop(1);
                        break;
                    case UsableLuaType.number:
                        result = lua.CheckNumber(-1);
                        lua.Pop(1);
                        break;
                    case UsableLuaType.luaString:
                        result = lua.CheckString(-1);
                        lua.Pop(1);
                        break;
                    case UsableLuaType.cfunction:
                        result = FetchCsFunction();
                        break;
                    case UsableLuaType.function:
                        result = FetchLuaFunction();
                        break;
                    case UsableLuaType.table:
                        result = FetchTable();
                        break;
                    case UsableLuaType.csobject:
                        result = ExCsRawData.FetchFromLuaStack(this);
                        break;
                    default:
                        throw new InvalidDataException("Invalid type:" + type);
                }
                return result;
            }
        }

    }
}